

from google.appengine.ext import db

from gae_utils import get_session

import hashlib
import logging

from pickle import dumps, loads
from gae_utils import plaintext2html

import csv
import re

# IMPORTANT: all dates are UTC, when being displayed need to wrap w/ localtime()
#            unless using as_time_ago b/c it's relative

class User(db.Model):
    username = db.StringProperty(required=True)
    password = db.StringProperty(required=True)
    
    admin = db.BooleanProperty(default=False)
    
    email = db.EmailProperty(required=True)
    name = db.StringProperty(default='')
    phone = db.PhoneNumberProperty()
    address = db.PostalAddressProperty()
    
    last_activity = db.DateTimeProperty(auto_now=True)
    created = db.DateTimeProperty(auto_now_add=True)
    
    # currently unused, don't need it, can just query by user
    # might be a way to easily backref it though
    # historical list of product lists
    #product_lists = db.ListProperty(db.Key)
    
    #session reserved
    
    def display_email(self):
        if self.name:
            return '%s <%s>' % (self.name, self.email)
        else:
            return self.email
    
    @classmethod
    def pwdhash(cls, pwd):
        return hashlib.sha1(pwd).hexdigest()
    
    @classmethod
    def by_username(cls, username):
        try:
            user = User.all().filter('username =', username).fetch(1)[0]
        except IndexError:
            user = None
        return user
    
    @classmethod
    def get_user(cls):
        session = get_session()
        if 'uid' not in session: return None
        try:
            user = User.get(session['uid'])
            user.session = session
            return user
        except Exception, e:
            logging.error(e)
        return None
    


class ProductList(db.Model):
    user = db.ReferenceProperty(User)
    
    # for admin's master set to None, for restaurants set to the master instance
    # for now, assuming master list not updated after sent out to restaurants
    #     b/c if they started editing and then the 
    source = db.SelfReferenceProperty()
    
    # currently only show to admin for reference, client's don't see this..
    name = db.StringProperty(required=True)
    notes = db.TextProperty()
    data = db.BlobProperty(required=True)
    
    updated = db.DateTimeProperty(auto_now=True)
    created = db.DateTimeProperty(auto_now_add=True)
    
    IMG_RE = re.compile(re.escape('[[')+'[^\]]*'+re.escape(']]'))

    def __init__(self, *args, **kwargs):
        db.Model.__init__(self, *args, **kwargs)
        self._cached_data = None

    def render_notes(self):
        return self.IMG_RE.sub(lambda m: '<img src="%s" />' % m.group().strip(']['),
                               plaintext2html(self.notes))

    @classmethod
    def get_buyer_list(cls, current_list=None, user=None):
        assert user and current_list, 'missing one of the required fields'
        
        # check if list already exists, if not create it
        plists = cls.all().filter('source =', current_list.key()).filter('user =', user.key()).fetch(1)
        if plists:
             buyer_list = plists[0]
             return buyer_list
        
        pl = ProductList(user=user, source=current_list, name=current_list.name, notes='',
                         data=current_list.data)
        pl.put()
        return pl

    @classmethod
    def create(cls, user=None, name='', notes='', data='', date=None, save=True):
        assert user and name and data, 'missing one of the required fields'
        assert user.admin, 'only admin accounts should create product lists w/ this method'
        
        pl = ProductList(user=user, source=None, name=name, notes=notes,
                         data=dumps(cls.parse(data)), created=date)
        if save: pl.put()
        return pl

    def get_data(self, as_order=False):
        # in parsed format
        if not self._cached_data:
            self._cached_data = loads(self.data)
        if as_order:
            return [r for r in self._cached_data if r['ORDER']]
        # there are misc empty rows used for formatting that we keep but don't show except on output
        # but this doesn't work b/c row #'s overwrite
        #return [r for r in self._cached_data if r['PRICE']]
        return self._cached_data 
    
    def save_data(self, data):
        self._cached_data = data
        self.data = dumps(data)

    def sale_total(self):
        total = 0.0
        for row in self.get_data():
            try:
                total += max(0.0, float(row['ORDER'] or 0.0)) * float(row['PRICE'].strip('$ '))
            except ValueError:
                # bad order data probably
                pass
        return total 
    
    def get_headers(self):
        return self.HEADERS

    @classmethod
    def get_current(cls):
        current_list = None
        plists = cls.all().filter('source =', None).order('-created').fetch(1)
        if plists:
             current_list = plists[0]
        return current_list

    # io methods

    HEADERS = ["ORDER","PRODUCT","Description","QUANTITY AVAILABLE","SOLD BY THE...","PRICE","FARM","PURCHASER"]

    
    # TODO: need individual order output & all orders combined outputs

    def output(self, headers=True, all=False):
        """Formats a restaurant order dict as a tab delimited string"""
        table = []
        if headers:
            table.append(self.get_headers())
        for row in loads(self.data):
            if row['ORDER']:
                row['PURCHASER'] = self.user.username
            if not all and not row['ORDER']:
                continue
            new_row = []
            for k in self.get_headers():
                new_row.append(row[k] or '')
            table.append([str(v) for v in new_row])
        
        # TODO: append buyer notes to output?
            
        return '\n'.join(['\t'.join(row) for row in table]) + '\n'

    @classmethod
    def parse(cls, data):
        """
        Parses a restaurant order from Google Docs into a list of dicts.
        
        Expects a string containing tab delimited restaurant order data cut and pasted
        from a spreadsheet, and returns a list of dictionaries of the heading name and 
        row value. 
        """
        
        # TODO: verify PRICE column values parseable
        #        and ORDER & PURCHASER should be empty..
            
        header = '\t'.join(cls.HEADERS)
        data = data.strip()
        
        # add headers if we don't have 'em
        if not data.startswith("ORDER"):
            data = header + "\n" + data
        
        data = data.split('\n')
        reader = csv.DictReader(data, delimiter = '\t')
        
        # XXX: make sure ORDER column is all 0's or empty..
        
        ret = [x for x in reader if type(x) == dict]
        for row in ret:
            if row['PRICE']:
                row['ORDER'] = 0
            row['PURCHASER'] = ''
        return ret


sampledata = """
ORDER	PRODUCT	Description	QUANTITY AVAILABLE	SOLD BY THE...	PRICE	FARM	PURCHASER
1	arugula	slight insect damage (small holes, very useable)	5 lb.	lb.	$12	Elm Tree Gardens	COW
	asian stir fry mix 	med to large leafed cabbage, kale, mizuna, choi, mustard	15 lb.	lb.	$4.5	Elm Tree Gardens	COW
2	asian stir fry mix 	med to large leafed cabbage, kale, mizuna, choi, mustard	15 lb.	lb.	$4.5	Zehr Family Farm	COW
3	beets, BABY	red - golf ball sized - tops on - by the BUNCH	50 bunch	bunch	$3	Fisher Farm	COW
	beets, BABY	red - golf ball sized - tops on - by the BUNCH	50 bunch	bunch	$3	Kawartha Organic Produce	COW
6	beets, BABY	red - golf ball sized - tops on - by the pound (loose)	50 lb. +	lb.	$3	Fisher Farm	COW
	beets, BABY	red - golf ball sized - tops on - by the pound (loose)	50 lb. +	lb.	$3	Kawartha Organic Produce	
	beets, BABY, WHITE 	golf ball sized - tops on - by the BUNCH	50 bunch	bunch	$3	Kawartha Organic Produce	
	beets, BABY, WHITE 	golf ball sized - tops on - by the pound (loose)	50 lb. +	lb.	$3	Kawartha Organic Produce	
"""
sampledata_noheader = """
1	arugula	slight insect damage (small holes, very useable)	5 lb.	lb.	$12	Elm Tree Gardens	COW
	asian stir fry mix 	med to large leafed cabbage, kale, mizuna, choi, mustard	15 lb.	lb.	$4.5	Elm Tree Gardens	COW
2	asian stir fry mix 	med to large leafed cabbage, kale, mizuna, choi, mustard	15 lb.	lb.	$4.5	Zehr Family Farm	COW
3	beets, BABY	red - golf ball sized - tops on - by the BUNCH	50 bunch	bunch	$3	Fisher Farm	COW
	beets, BABY	red - golf ball sized - tops on - by the BUNCH	50 bunch	bunch	$3	Kawartha Organic Produce	COW
6	beets, BABY	red - golf ball sized - tops on - by the pound (loose)	50 lb. +	lb.	$3	Fisher Farm	COW
	beets, BABY	red - golf ball sized - tops on - by the pound (loose)	50 lb. +	lb.	$3	Kawartha Organic Produce	
	beets, BABY, WHITE 	golf ball sized - tops on - by the BUNCH	50 bunch	bunch	$3	Kawartha Organic Produce	
	beets, BABY, WHITE 	golf ball sized - tops on - by the pound (loose)	50 lb. +	lb.	$3	Kawartha Organic Produce	
"""


if __name__ == "__main__":
    """
    _data = parse(sampledata)
    print display(_data)
    print tabular(_data)
    """
    pass

 
